package sk.tuke.kephera.connector;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import sk.tuke.kephera.observer.ObservedSubject;
import sk.tuke.kephera.observer.Observer;

/**
 * Konektor na Kephera platformu, chova se podle protokolu dle Patrikova AK agent komunikace. Pro posilani
 * strukturovanych zprav se pouzivat {@link KepheraMsg}
 * 
 * @author MAR
 * @version 1.0
 * 
 */
public class KepheraConnector implements ObservedSubject<KepheraMsg> {
    private final String broadcastAddr;
    private static boolean log = false;
    private DatagramSocket socket;
    private InetAddress receiverAddress;
    private int port;
    private Map<String, KepheraMsg> schranka = new ConcurrentHashMap<String, KepheraMsg>();
    private String localMachineName;
    private volatile List<Observer<KepheraMsg>> observerList = new ArrayList<Observer<KepheraMsg>>();

    private KepheraConnector(String localMachineName, String broadcastAddress, int port) {
        // factory pattern
        this.broadcastAddr = broadcastAddress;
        this.port = port;
        this.localMachineName = localMachineName;
    }

    public static void setLog(boolean isEnabled) {
        log = isEnabled;
    }

    /**
     * Vytvori konektor ktery posila a naslucha na dany port, broadcast adresa je 255.255.255.255
     * 
     * @param localMachineName
     *            indentifikator lokalniho pc
     * @param port
     *            pro prijem a odesilani
     * @return
     */
    public static KepheraConnector createUdpListeningConnector(String localMachineName, int port) {
        KepheraConnector kepheraConnector = new KepheraConnector(localMachineName, "255.255.255.255", port);
        return kepheraConnector;
    };

    public static KepheraConnector createUdpListeningConnector(String localMachineName, String broadcastAdr, int port) {
        KepheraConnector kepheraConnector = new KepheraConnector(localMachineName, broadcastAdr, port);
        return kepheraConnector;
    };

    /**
     * Pripojeni k soketu, at uz k TCP nebo k UDP. Z pohledu funkcionality je dulezite volat connect pred komunikaci a
     * disconnect po. Jen z duvodu ze nejsme omezeni implementace na UDP nebo TCP. Pri UDP se sice nepripojuje, ale
     * vyrabi se soket
     */
    public void connect() {
        try {
            socket = new DatagramSocket(port); // some port
            log("soket created");
            receiverAddress = InetAddress.getByName(broadcastAddr);
            log("Local Host: " + InetAddress.getLocalHost());

        } catch (SocketException | UnknownHostException e) {
            log("ERR:" + e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /**
     * Prijeti zpravy ze soketu dane protokolem. Deafultni timeout 1 vterina
     * 
     * @param msgType
     * @return
     */
    public KepheraMsg receive(int msgType) {
        return receive(msgType, 1000);
    }

    /**
     * Prijeti zpravy ze soketu dane protokolem
     * 
     * @return
     */
    public KepheraMsg receive(int msgType, int timeoutInMilis) {
        try {
            while (true) {
                String klic = String.valueOf(msgType);
                if (schranka.keySet().contains(klic)) {
                    return schranka.remove(klic);
                }
                byte[] buf = new byte[2048];
                DatagramPacket packet = new DatagramPacket(buf, buf.length);
                log("start receiving data");
                socket.setSoTimeout(timeoutInMilis);
                socket.receive(packet);
                String odpovedTxt = new String(packet.getData());
                log("received data:" + odpovedTxt);
                KepheraMsg odpoved = KepheraUtils.fromJson(odpovedTxt);

                notifyObservers(odpoved);

                if ((odpoved.getType() * -1) == msgType && odpoved.getTo().contains(this.localMachineName)) {
                    return odpoved;
                } else {
                    schranka.put(String.valueOf((odpoved.getType() * -1)), odpoved);
                }
            }
        } catch (Exception e) {
            log("ERR:" + e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /**
     * Odesila zpravy dane protokolem
     * 
     * @param protokol
     */
    public synchronized void send(KepheraMsg protokol) {
        try {
            String data = KepheraUtils.toJson(protokol);
            log("sending data:" + data);
            byte[] dataInBytes = data.getBytes();
            DatagramPacket packet = new DatagramPacket(dataInBytes, dataInBytes.length, receiverAddress,
                    port);
            socket.send(packet);
        } catch (Exception e) {
            log(e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /**
     * Odpojeni a ukonceni komunikace
     */
    public void disconnect() {
        socket.close();
        log("socket closed");
    }

    /**
     * Loguje udalosti na standardni vystup zatim
     * 
     * @param txt
     */
    private void log(String txt) {
        if (log) {
            System.out.println("kc > " + txt);
        }
    }

    @Override
    public boolean addObserver(Observer<KepheraMsg> o) {
        return observerList.add(o);
    }

    @Override
    public boolean removeObserver(Observer<KepheraMsg> o) {
        return observerList.remove(o);
    }

    @Override
    public void notifyObservers(KepheraMsg msg) {
        for (Observer<KepheraMsg> o : observerList) {
            o.handle(msg);
        }
    }
}
